#import	"SpinView.h"

#import	<libc.h>
#import	<math.h>
#import <sys/time.h>
#import	<defaults/defaults.h>

#import	<dpsclient/dpsclient.h>
#import	<dpsclient/wraps.h>

#import	<appkit/Slider.h>

/**********************************************************************/

#define PI				( 3.1415926 )

#define	DEGtoRAD(deg)	( deg * PI/180.0 )

#define	LEFT			( 100 )
#define	RIGHT			( 101 )
#define	UP				( 102 )
#define	DOWN			( 103 )

/**********************************************************************/

//	Looks like a set of defaults to me, how 'bout you?

static	NXDefaultsVector	SpinDefaults = 
	{
		{ "Radius", "104" },
		{ "TailLength", "95" },
		{ "XIncrement", "1.0" },
		{ "YIncrement", "2.5" },
		{ "AngleIncrement", "38" },
		{ "NumberOfSpinners", "4" },
		{ NULL },
	};

/**********************************************************************/

//	userPathData holds four endpoint coordinates for each line to draw.
//	The endpoint coordinates are calculated on the fly, in drawSpinners:.
//	userPathOps holds a moveto/lineto pair for each line that can be drawn.
//	Both of these are passed to DPSDoUserPath(), along with the other
//	neccessary arguments.  See the method drawSpinners: for the call.

static	float	userPathData[ 4*MAXNUMSPINS ];

static	char	userPathOps[ ] =
	{
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto,
		dps_moveto, dps_lineto
	};

static	float	Sines[ 91 ];		// Table of sines.  Filled in initFrame:

static	float	SinRad[ 91 ];		// Table of (sines * current radius).
									// Filled in setStartPoints.

static	char	tnum[ 20 ];			// Used to modify defaults, if they
									// need to be adjusted in the database.
	
/**********************************************************************/

@implementation SpinView
 
/**********************************************************************/

- (const char *) windowTitle
{
	return ( const char * ) "Larrys Wild Spinners";
}

/**********************************************************************/

- ( void ) getAngleInc
{
	//	Gets the current angle increment from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXANGLEINC and MINANGLEINC.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurAngleInc = atoi( NXGetDefaultValue( OWNER, "AngleIncrement" ) );
	
	if( CurAngleInc > MAXANGLEINC )
	{
		sprintf( tnum, "%3d", MAXANGLEINC );
		NXWriteDefault( OWNER, "AngleIncrement", tnum );
		CurAngleInc = MAXANGLEINC;
	}
	else if( CurAngleInc < MINANGLEINC )
	{
		sprintf( tnum, "%3d", MINANGLEINC );
		NXWriteDefault( OWNER, "AngleIncrement", tnum );
		CurAngleInc = MINANGLEINC;
	}
}

/**********************************************************************/

- ( void ) getNumSpinners
{
	//	Gets the current number of spinners from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXNUMSPINS and MINNUMSPINS.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurNumSpins = atoi( NXGetDefaultValue( OWNER, "NumberOfSpinners" ) );
	
	if( CurNumSpins > MAXNUMSPINS )
	{
		sprintf( tnum, "%2d", MAXNUMSPINS );
		NXWriteDefault( OWNER, "NumberOfSpinners", tnum );
		CurNumSpins = MAXNUMSPINS;
	}
	else if( CurNumSpins < MINNUMSPINS )
	{
		sprintf( tnum, "%2d", MINNUMSPINS );
		NXWriteDefault( OWNER, "NumberOfSpinners", tnum );
		CurNumSpins = MINNUMSPINS;
	}
}

/**********************************************************************/

- ( void ) getRadius
{
	//	Gets the current circle radius from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXRADIUS and MINRADIUS.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurRadius = atoi( NXGetDefaultValue( OWNER, "Radius" ) );
	
	if( CurRadius > MAXRADIUS )
	{
		sprintf( tnum, "%3d", MAXRADIUS );
		NXWriteDefault( OWNER, "Radius", tnum );
		CurRadius = MAXRADIUS;
	}
	else if( CurRadius < MINRADIUS )
	{
		sprintf( tnum, "%3d", MINRADIUS );
		NXWriteDefault( OWNER, "Radius", tnum );
		CurRadius = MINRADIUS;
	}
}

/**********************************************************************/

- ( void ) getTailLength 
{
	//	Gets the current tail length from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXTAILLEN and MINTAILLEN.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurTailLen = atoi( NXGetDefaultValue( OWNER, "TailLength" ) );
	
	if( CurTailLen > MAXTAILLEN )
	{
		sprintf( tnum, "%3d", MAXTAILLEN );
		NXWriteDefault( OWNER, "TailLength", tnum );
		CurTailLen = MAXTAILLEN;
	}
	else if( CurTailLen < MINTAILLEN )
	{
		sprintf( tnum, "%3d", MINTAILLEN );
		NXWriteDefault( OWNER, "TailLength", tnum );
		CurTailLen = MINTAILLEN;
	}
}

/**********************************************************************/

- ( void ) getXIncrement 
{
	//	Gets the current X increment from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXXINC and MINXINC.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurXInc = atof( NXGetDefaultValue( OWNER, "XIncrement" ) );
	
	if( CurXInc > MAXXINC )
	{
		sprintf( tnum, "%7.3f", MAXXINC );
		NXWriteDefault( OWNER, "XIncrement", tnum );
		CurXInc = MAXXINC;
	}
	else if( CurXInc < MINXINC )
	{
		sprintf( tnum, "%7.3f", MINXINC );
		NXWriteDefault( OWNER, "XIncrement", tnum );
		CurXInc = MINXINC;
	}
}

/**********************************************************************/

- ( void ) getYIncrement 
{
	//	Gets the current Y increment from the defaults database.
	//	Checks to make sure the value found is within the range delimited
	//	by MAXYINC and MINYINC.  If the value is out of range it
	//	is reset to the nearest inbounds value.
		
	CurYInc = atof( NXGetDefaultValue( OWNER, "YIncrement" ) );
	
	if( CurYInc > MAXYINC )
	{
		sprintf( tnum, "%7.3f", MAXYINC );
		NXWriteDefault( OWNER, "YIncrement", tnum );
		CurYInc = MAXYINC;
	}
	else if( CurYInc < MINYINC )
	{
		sprintf( tnum, "%7.3f", MINYINC );
		NXWriteDefault( OWNER, "YIncrement", tnum );
		CurYInc = MINYINC;
	}
}

/**********************************************************************/

- initFrame : ( const NXRect * ) frameRect
{
	//	How many miscellaneous setup things can you 
	//	put into a single method, geez!
	
	struct	timeval		tp;
	struct	timezone	tzp;
	int	i;

	[ super initFrame : frameRect ];
	
	[ self setOpaque : YES ];
	[ self setClipping : NO ];
	
	NXRegisterDefaults( OWNER, SpinDefaults );
	
	[ self getNumSpinners ];
	[ self getRadius ];
	[ self getAngleInc ];
	[ self getTailLength ];
	[ self getXIncrement ];
	[ self getYIncrement ];
	
	PSsetlinewidth( 0.15 );

	gettimeofday( &tp, &tzp );
	srandom( ( int ) tp.tv_sec + tp.tv_usec );
			
	for( i = 0; i < 91; ++i )
		Sines[ i ] = sin( DEGtoRAD(i) );		// Fill in sines table.

	[ self resetSpinners ];
	
	return self;
}

/**********************************************************************/

- newWindow
{
	[ self resetSpinners ];
	
	return self;
}

/**********************************************************************/

- sizeTo : ( NXCoord ) width : ( NXCoord ) height
{
	[ super sizeTo : width : height ];
	
	[ self resetSpinners ];
	
	return self;
}

/**********************************************************************/

- ( void ) setStartPoints
{
	int	i;
	
	for( i = 0; i < CurNumSpins; ++i )
	{
		headSpin[i].x = tailSpin[i].x = random() % ( (int) bounds.size.width);
		headSpin[i].y = tailSpin[i].y = random() % ( (int) bounds.size.height);
	}
	
	//	Reset the color cycle.
	
	r = 0;
	g = ( 2 * PI ) / 3;
	
	// Everytime the current radius changes the SinRad[] table and the
	// bounding box need to be updated.  These should probably be done
	// in the setRadius: method, but it's so fast I don't think it matters.
	
	for( i = 0; i < 91; ++i )
		SinRad[ i ] = CurRadius * Sines[ i ];

	// Setting the bounding box this way gives the spinners a more consistent
	// feel.  When there is only one spinner displayed the speed of the spinner
	// appears near the speed of having 20 spinners on the screen.  Using a
	// consistent bounding box has a lot to do with it.
	// The 30 is a fudge factor, I guess I should figure out the real value
	// at some point.
	
	Bbox[ 0 ] = bounds.origin.x - ( CurRadius + 30 );
	Bbox[ 1 ] = bounds.origin.y - ( CurRadius + 30 );
	Bbox[ 2 ] = bounds.origin.x + bounds.size.width + ( CurRadius + 30 );
	Bbox[ 3 ] = bounds.origin.y + bounds.size.height + ( CurRadius + 30 );
	
}

/**********************************************************************/

- ( void ) resetSpinners
{
	// This method realigns the head and tail spinner data structures.
	// Each time a control is adjusted this method is called.  It is much
	// simpler to reset everything after a control is adjusted than
	// it is to figure out how to align things on the fly.
	
	SPINNER	*hs;
	SPINNER	*ts;
	int		iMod2;
	int		i;
	
	[ self setStartPoints ];
	
	hs = headSpin;	// Moving pointers through the arrays is a lot faster.
	ts = tailSpin;
	
	for( i = 0; i < CurNumSpins; ++i, ++hs, ++ts )
	{
		hs->a  = ts->a  = random() % 360;
		hs->ix = ts->ix = CurXInc;
		hs->iy = ts->iy = CurYInc;
		
		iMod2 = i % 2;
		hs->dx = ts->dx = ( ( iMod2 ) ? RIGHT : LEFT );
		hs->dy = ts->dy = ( ( iMod2 ) ? DOWN : UP );
	}
	
	if( tailSlider )
		CurTailLen = [ tailSlider intValue ];
}

/**********************************************************************/
	
- ( void ) moveSpinner : ( SPINNER * ) spin
{
	spin->a = ( spin->a + CurAngleInc ) % 360;
	
	if( spin->x >= bounds.size.width )
		spin->dx = LEFT;
	else if( spin->x <= 0 )
		spin->dx = RIGHT;
	
	spin->x += ( spin->dx == RIGHT ) ? spin->ix : -spin->ix;

	if( spin->y >= bounds.size.height )
		spin->dy = DOWN;
	else if( spin->y <= 0 )
		spin->dy = UP;
	
	spin->y += ( spin->dy == UP ) ? spin->iy : -spin->iy;
}

/**********************************************************************/

- ( void ) drawSpinners : ( SPINNER * ) spin
{
	// I know this looks a little complicated, but it really
	// just builds a user path.  It fills the userPathData[] array
	// with enough endpoints to draw CurNumSpins lines.  Once userPathData[]
	// is filled its passed to DPSDoUserPath(), along with the count of
	// points in the array.
	
	float	offset1;
	float	offset2;
	int		i, j;
	
	for( i = 0, j = 0; i < CurNumSpins; ++i, ++spin )
	{
		if( spin->a >= 270 )
		{
			offset1 = -SinRad[ 360 - spin->a ];
			offset2 =  SinRad[ spin->a - 270 ];
		}
		else if( spin->a >= 180 )
		{
			offset1 = -SinRad[ spin->a - 180 ];
			offset2 = -SinRad[ 270 - spin->a ];
		}
		else if( spin->a >= 90 )
		{
			offset1 =  SinRad[ 180 - spin->a ];
			offset2 = -SinRad[ spin->a - 90 ];
		}
		else
		{
			offset1 = SinRad[ spin->a ];
			offset2 = SinRad[ 90 - spin->a ];
		}
		
		userPathData[ j++ ] = spin->x + offset2;
		userPathData[ j++ ] = spin->y + offset1;
		userPathData[ j++ ] = spin->x - offset2;
		userPathData[ j++ ] = spin->y - offset1;
	
		[ self moveSpinner : spin ];
	}

	DPSDoUserPath( userPathData, j, dps_float, userPathOps,
				   CurNumSpins*2, Bbox, dps_ustroke );
}

/**********************************************************************/

- oneStep
{
	if( CurTailLen-- <= 0 )
	{
		PSsetrgbcolor( 0.0, 0.0, 0.0 );

		[ self drawSpinners : tailSpin ];
	}
	
//	PSsetgray( NX_WHITE );		// Used only for testing.
	
	r -= ( r > 2 * PI ) ? 2 * PI : -0.008;
	g -= ( g > 2 * PI ) ? 2 * PI : -0.02;
	b -= ( b > 2 * PI ) ? 2 * PI : -0.01;
	
	PSsetrgbcolor( ( cos( r ) + 1.0 ) * .5,
				   ( cos( g ) + 1.0 ) * .5,
				   ( cos( b ) + 1.0 ) * .5 );  
                
	[ self drawSpinners : headSpin ];

	return self;
}

/**********************************************************************/

- drawSelf : ( NXRect * ) rect : ( int ) count
{	
	if (!rect || !count)
		return self;
		
	PSsetgray( NX_BLACK );
	
	NXRectFill( rect );

	return self;
	
}

/**********************************************************************/

@end
